iT邦幫忙

2023 iThome 鐵人賽

DAY 30
1
自我挑戰組

WiFiBoy Python 玩學機介紹系列 第 30

30. 進階主題-Forth模擬器

  • 分享至 

  • xImage
  •  

什麼是 Forth 語言

Forth 語言是 1960 年代末期,由 Charles H. Moore 發展出來,做為天文台電腦自動控制與程式設計的一種程式語言。在80 年代,一群愛好者成立了 Forth Interesting Group(FIG),在世界各地推廣,並在各類電腦上建立 Forth 系統與標準語言。

Forth 以可延伸的辭典為核心,使用堆疊(Stack)為基礎,是一種高度模組化的程式語言,更特別的是一種將解譯器與編譯器合併使用的系統。在開發的過程中,可在編譯過程裡檢查系統狀態與錯誤,並且逐步擴展程式功能。

因為符式語言使用後敘式,跟我們常用的中序式不太一樣,也是一般使用者不容易入門的原因。舉個實例來說,我們平常用 MicroPython 計算 (3+7) * 5 / 2 ,使用 Forth 語言則要改寫成 3 7 + 5 * 2 /,有沒有發覺什麼地方不一樣呢?是不是括弧都去掉了?雖然輸入的順序不太一樣,但可以省略掉這麼多括弧,是不是覺得 Forth 語言很特別?

各位讀者如果對於 Forth 語言有興趣,可以參考 Forth Inc. 的 「Starting Forth」的入門書,您會對於 Forth 這個語言有著更深一層的認識。

Forth 語言的應用領域

我們可以從幾個實例來了解Forth 語言的優越性,「簡單就是好」。因為系統核心簡單,容易專注在要處理的問題上。

太空應用: 歐盟的 Philae 衛星 https://en.wikipedia.org/wiki/Philae_(spacecraft) 上面的主要 CPU ,使用的是 RTX 2010 。

https://ithelp.ithome.com.tw/upload/images/20231001/20105707B4hb6Rv4mi.png

https://ithelp.ithome.com.tw/upload/images/20231001/20105707cgmlWLFZZs.png

PowerPC 時代的蘋果電腦,開機時按住 command + Option + O + F 鍵,會進入 Open Firmware 環境。這個 Open Firmware 底層就是使用 Forth 語言開發的。您可以使用內建的相關指令,對於整台麥金塔電腦進行硬體檢測。甚至是開發應用程式或遊戲。

https://ithelp.ithome.com.tw/upload/images/20231001/20105707dhkd2GNMiF.png

https://ithelp.ithome.com.tw/upload/images/20231001/20105707bDBBsUC11P.jpg

2003 年左右,丁陳漢蓀博士出版了「嵌入式系統,使用eForth」這本書,告訴大家如何使用Forth 語言與 VHDL 硬體描述語言,設計一台電腦的硬體、系統程式與應用軟體。

https://ithelp.ithome.com.tw/upload/images/20231001/20105707M3JXPbcYjS.png

如何用 Python 寫一個 Forth 語言模擬器

以我們的程度要寫一個 Forth 語言模擬器是相當困難的事情。因此在網頁上尋找相關範例是比較可行的做法。過去筆者曾參加過臺灣符式推廣協會的活動,知道該會會員曾經寫過 JeForth 與 PeForth 這兩個以別種程式語言開發 Forth 模擬環境的系統。陳厚成先生編寫的 PeForth 環境也有收入到 Python 套件庫中,您可以在 Thonny 的套件管理程式搜尋並安裝。

https://ithelp.ithome.com.tw/upload/images/20231001/20105707LcK5K9bUeq.png

不過 PeForth 功能強大,但原始碼超過兩千行,初學者實在是無法讀懂。筆者又在另外一個教學網站中找到程式碼更少,解釋註解詳細的範例程式,僅有176行程式。請各位讀者參考 https://www.openbookproject.net/py4fun/forth/forth.html

可以試著用瀏覽器的翻譯功能,來看懂裡面的解說。

https://ithelp.ithome.com.tw/upload/images/20231001/20105707xC02Oe67fF.png

Forth 模擬器的程式碼在此:

https://www.openbookproject.net/py4fun/forth/forth.py

證明MicroPython 有多好用

一開始,筆者也很擔心要將純 Python 開發的模擬器移植到玩學機上很困難。移植程式到新環境上常會遇到幾個問題:

  1. 硬體架構上的差異,桌上型電腦跟微處理器的運算速度與記憶體相差懸殊。
  2. 雙方雖然都支援Python開發環境,但MicroPython 支援的函數數量是遠低於一版的Python環境。
  3. 模擬器作者對於Python系統與要模擬的系統 Forth都要有一定程度熟悉。
  4. 模擬器的程式碼還不能寫得太長。這是未來系統要做為系統核心的程式,如果太長,以後使用者要開發應用軟體就會出現記憶體與空間不足的問題。

經過實驗後,筆者發現自己真的是想太多。原則上只要將原始程式的第5行與第16、17行註解掉,然後另存為 F.py 。再將此檔案上傳到玩學機的檔案系統中。就可以讓 Forth 的模擬器在玩學機上執行。

範例實作

修改提示訊息

我們把原程式第 102 行
pcode = []; prompt = "Forth> " 修改為 pcode = []; prompt = "OK> "
符合Forth 語言編譯器的慣例。

新增Forth指令

符式語言系統內分為系統可以立即執行的字(指令)或需要編譯過的字(指令)。我們在這個範例中,教大家如何定義可以立即執行的字。在原始程式,是從第52行開始一連串定義了類似

def rAdd (cod,p) : b=ds.pop(); a=ds.pop(); ds.append(a+b)

這樣的定義。

還記不記得 Forth 要怎麼計算加法? 原本 1 + 3 要改成 1 3 + 。那我們要怎麼用Python 語言來定義這個符式字呢?看看上面這段程式範例,我們來逐個指令解釋:

  1. 符式語言的環境用這個 + 號來表示加法,但 Python 環境沒辦法直接用 + 號來當作函數名稱,所以我們定義 rAdd作為函數名稱。
  2. 因為符式語言的資料操作是靠資料堆疊 Data Stack 來操作。因此我們要計算 a + b 就必須先把 b 這個數字送入到資料堆疊,再將a送進去,最後接到使用者傳入加號,就將資料堆疊上面兩個元素彈出,進行 a+b 的運算,最後a+b的值重新放入資料堆疊中,就完成了加法指令的操作。在Python 語言中,我們需要使用pop() 方法來彈出堆疊中的元素,使用append() 方法將結果放入資料堆疊裡。

我們可以試著做一個一次複製兩個堆疊元素的 2Dup 指令,建造方法如下:

def r2Dup(cod,p) : ds.append(ds[-2]); ds.append(ds[-2])

指令建造好後,要在 rDict 物件中加入指令名稱,這樣 Forth 引擎才會找得到您定義的字,系統才可以正常執行該指令。

https://ithelp.ithome.com.tw/upload/images/20231001/20105707dY4IaxJlTk.png

最後將 F.py 存檔,傳到玩學機後,重新啟動玩學機,到命令列下執行

import F as F
F.main()

就會叫出 Forth 模擬器,您就可以用 Forth 語言來開發應用程式。

https://ithelp.ithome.com.tw/upload/images/20231001/20105707vJLWj4p7bR.png

包裝 WiFiBoy 上的功能變成 Forth 引擎認得的字

我們試著做幾個 Forth 字來控制玩學機:

  1. rCLS 清除螢幕。
  2. rRED 定義紅色的值
  3. rSCR_RED 以紅色來清除螢幕
  4. rLine 來呼叫玩學機中的劃線指令

程式定義如下:

https://ithelp.ithome.com.tw/upload/images/20231001/20105707DOne2GmePr.png

檔案存檔,重開機並啟動Forth 環境後,就可以執行相關的定義字。

清除畫面,請執行 CLS。這時您會發現螢幕會變成全黑的畫面。
要將畫面清除成紅色背景,請執行 SCR_RED。

要畫一條紅色的線,則需要輸入 X1, Y1, X2, Y2 與線段顏色 五個參數,請參考底下的指令來執行

CLS
0 0 64 60 RED LINE

這時您就會看到玩學機中畫了一條紅色的線。

https://ithelp.ithome.com.tw/upload/images/20231001/20105707oO9NpdMbPC.png

讀者可以試著定義更多的指令,您就會了解使用 Forth 開發軟體的好處。
Forth 引擎範例程式
本程式是修改自 https://www.openbookproject.net/py4fun/forth/forth.html 的原程式,未來在使用與散佈程式請記得尊重原作者的智慧財產權,請務必說明相關引用資訊。

# forth.py
# 原始程式: 引用自 https://www.openbookproject.net/py4fun/forth/forth.html
# 程式修改: Samsuan Chen, Derek Lai,  Daniel Teng 2023.09
# =========================================================================
# ============================= 引用模組
import sys, re
import wifiboy as wb
#============================= 全域變數宣告
ds       = []          # The data stack
cStack   = []          # The control struct stack
heap     = [0]*20      # The data heap
heapNext =  0          # Next avail slot in heap
words    = []          # The input stream of tokens
initCode = ""
#============================= 主程式
def main() :
    global words, initCode
#     if len(sys.argv) > 1 :
#         initCode = open(sys.argv[1]).read()   # load start file
# MicroPython 不支援直接使用 python F.py 加入參數的使用法
    while True :
        pcode = compile()          # compile/run from user
        if pcode == None : print(""); return
        execute(pcode)
#============================== Lexical Parsing
def getWord (prompt="... ") :
    global words, initCode
    while not words : 
        try :
            if initCode : lin = initCode; initCode=""
            else        : lin = input(prompt)+" "
        except : return None
        tokenizeWords(lin)
    word = words[0]
    if word == "bye" : return None
    words = words[1:]
    return word
def tokenizeWords(s) :
    global words                                          # clip comments, split to list of words
    words += re.sub("#.*\n","\n",s+"\n").lower().split()  # Use "#" for comment to end of line
#================================= Runtime operation
def execute (code) :
    p = 0
    while p < len(code) :
        func = code[p]
        p += 1
        newP = func(code,p)
        if newP != None : p = newP
# ================================ 定義字(可直接執行) ================================
def rAdd (cod,p) : b=ds.pop(); a=ds.pop(); ds.append(a+b)
def rMul (cod,p) : b=ds.pop(); a=ds.pop(); ds.append(a*b)
def rSub (cod,p) : b=ds.pop(); a=ds.pop(); ds.append(a-b)
def rDiv (cod,p) : b=ds.pop(); a=ds.pop(); ds.append(a/b)
def rEq  (cod,p) : b=ds.pop(); a=ds.pop(); ds.append(int(a==b))
def rGt  (cod,p) : b=ds.pop(); a=ds.pop(); ds.append(int(a>b))
def rLt  (cod,p) : b=ds.pop(); a=ds.pop(); ds.append(int(a<b))
def rSwap(cod,p) : a=ds.pop(); b=ds.pop(); ds.append(a); ds.append(b)
def rDup (cod,p) : ds.append(ds[-1])
def rDrop(cod,p) : ds.pop()
def rOver(cod,p) : ds.append(ds[-2])
# ========= Sam Start
def r2Dup(cod,p) : ds.append(ds[-2]); ds.append(ds[-2])
# def r2Dup (cod,p) : rOver(cod,p); rOver(cod,p)
# ========= Sam End
def rDump(cod,p) : print("ds = %s" % ds)
def rDot (cod,p) : print(ds.pop())
def rJmp (cod,p) : return cod[p]
def rJnz (cod,p) : return (cod[p],p+1)[ds.pop()]
def rJz  (cod,p) : return (p+1,cod[p])[ds.pop()==0]
def rRun (cod,p) : execute(rDict[cod[p]]); return p+1
def rPush(cod,p) : ds.append(cod[p])     ; return p+1
def rCreate (pcode,p) :
    global heapNext, lastCreate
    lastCreate = label = getWord()      # match next word (input) to next heap address
    rDict[label] = [rPush, heapNext]    # when created word is run, pushes its address
def rDoes (cod,p) :
    rDict[lastCreate] += cod[p:]        # rest of words belong to created words runtime
    return len(cod)                     # jump p over these
def rAllot (cod,p) :
    global heapNext
    heapNext += ds.pop()                # reserve n words for last create
def rAt  (cod,p) : ds.append(heap[ds.pop()])       # get heap @ address
def rBang(cod,p) : a=ds.pop(); heap[a] = ds.pop()  # set heap @ address
def rComa(cod,p) :                                 # push tos into heap
    global heapNext
    heap[heapNext]=ds.pop()
    heapNext += 1
# ===================== Daniel 定義 Start =========================================
# def rdotS (cod,p) : print("ds = %s" % ds);
def rCLS  (cod,p) : wb.cls();                       # 清除螢幕
def rRED (cod,p) : ds.append(wb.RED);
def rSCR_RED  (cod,p) : wb.cls(wb.RED);             # 將螢幕清成紅色
def rLINE(cod, p):
    print("Please input (x1, y1, x2, y2, color)!", len(ds))
    if len(ds) >= 5:
        color = ds.pop()
        y2 = ds.pop()
        x2 = ds.pop()
        y1 = ds.pop()
        x1 = ds.pop()
        wb.line(x1,y1,x2,y2,color)
# ===================== Daniel 定義 End ==================================
rDict = {
  '+'  : rAdd, '-'   : rSub, '/' : rDiv, '*'    : rMul,   'over': rOver,
  'dup': rDup, 'swap': rSwap, '.': rDot, 'dump' : rDump,  'drop': rDrop, 
  '='  : rEq,  '>'   : rGt,   '<': rLt,     
  ','  : rComa,'@'   : rAt, '!'  : rBang,'allot': rAllot,
  'create': rCreate, 'does>': rDoes,
  # ==================== Samsuanchen 新增
  '2dup' : r2Dup,
  # ==================== Daniel Teng 新增
  '.s' : rDump, 'cls' : rCLS, 'scr_red' : rSCR_RED, 'red' : rRED, 'line' : rLINE,
}
#================================= Compile time 
def compile() :
    pcode = []; prompt = "OK "  # 修改成符合 Forth 語言的習慣,OK 代表一切安好。
    while 1 :
        word = getWord(prompt)  # get next word
        if word == None : return None
        cAct = cDict.get(word)  # Is there a compile time action ?
        rAct = rDict.get(word)  # Is there a runtime action ?

        if cAct : cAct(pcode)   # run at compile time
        elif rAct :
            if type(rAct) == type([]) :
                pcode.append(rRun)     # Compiled word.
                pcode.append(word)     # for now do dynamic lookup
            else : pcode.append(rAct)  # push builtin for runtime
        else :
            # Number to be pushed onto ds at runtime
            pcode.append(rPush)
            try : pcode.append(int(word))
            except :
                try: pcode.append(float(word))
                except : 
                    pcode[-1] = rRun     # Change rPush to rRun
                    pcode.append(word)   # Assume word will be defined
        if not cStack : return pcode
        prompt = "...    "               # 指令還沒輸入完成
def fatal (mesg) : raise mesg
def cColon (pcode) :
    if cStack : fatal(": inside Control stack: %s" % cStack)
    label = getWord()
    cStack.append(("COLON",label))  # flag for following ";"
def cSemi (pcode) :
    if not cStack : fatal("No : for ; to match")
    code,label = cStack.pop()
    if code != "COLON" : fatal(": not balanced with ;")
    rDict[label] = pcode[:]       # Save word definition in rDict
    while pcode : pcode.pop()
def cBegin (pcode) :
    cStack.append(("BEGIN",len(pcode)))  # flag for following UNTIL
def cUntil (pcode) :
    if not cStack : fatal("No BEGIN for UNTIL to match")
    code,slot = cStack.pop()
    if code != "BEGIN" : fatal("UNTIL preceded by %s (not BEGIN)" % code)
    pcode.append(rJz)
    pcode.append(slot)
# ==================== SamSuanChen =====   Start
def cAgain (pcode) :                                     # 無窮迴圈
    if not cStack : fatal("No BEGIN for AGAIN to match")
    code,slot = cStack.pop()
    if code != "BEGIN" : fatal("AGAIN preceded by %s (not BEGIN)" % code)
    pcode.append(rJmp)
    pcode.append(slot)
# ==================== SamSuanChen =====   End
def cIf (pcode) :
    pcode.append(rJz)
    cStack.append(("IF",len(pcode)))  # flag for following Then or Else
    pcode.append(0)                   # slot to be filled in

def cElse (pcode) :
    if not cStack : fatal("No IF for ELSE to match")
    code,slot = cStack.pop()
    if code != "IF" : fatal("ELSE preceded by %s (not IF)" % code)
    pcode.append(rJmp)
    cStack.append(("ELSE",len(pcode)))  # flag for following THEN
    pcode.append(0)                     # slot to be filled in
    pcode[slot] = len(pcode)            # close JZ for IF

def cThen (pcode) :
    if not cStack : fatal("No IF or ELSE for THEN to match")
    code,slot = cStack.pop()
    if code not in ("IF","ELSE") : fatal("THEN preceded by %s (not IF or ELSE)" % code)
    pcode[slot] = len(pcode)             # close JZ for IF or JMP for ELSE

cDict = {
  ':'    : cColon, ';'    : cSemi, 'if': cIf, 'else': cElse, 'then': cThen,
  'begin': cBegin, 'until': cUntil, 'again' : cAgain,
}
  
if __name__ == "__main__" :
    main()

上一篇
# 29. 建立玩學機 Arduino 開發環境
系列文
WiFiBoy Python 玩學機介紹30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言